Android百问百答-《那些年被问过的Handler原理》#
文章迁移自我的csdn博客
关于Handler,安卓面试最热门的知识点之一。本篇文章将围绕3点展开:
可以提问哪些Questions?
面试官会怎样follow up?
以及怎样寻找答案。
Handler常见提问#
- 哪些场景使用到了Handler?用Handler做什么业务?
- 用Handler遇到什么问题?怎么解决这些问题的?
- 说一说Handler原理?
- 能自己实现一个Handler吗?
- 说一说Handler延时原理?
- Handler延时有哪些缺陷?造成这些缺陷的原因?
- 你知道Handler#handleMessage原理吗?
- Handler的post与sendMessage有哪些区别?
- 子线程能使用Handler吗?
- 子线程能创建Handler吗?
- 了解过HandlerThread吗?
- 了解过IdleHandler吗?
Handler常见Follow Up#
- 你刚才提到了Message,消息屏障听过吗?有几种Message?
- Message有什么用?存储了哪些信息?以什么数据结构存储?
- APP内最多能有几个Handler?
- App内最多能有多少Message?
- App内最多能有几个Looper?
- App内最多能有几个MessageQueue?
- Message如何知道发给哪一个MessageQueue?发给哪个Handler?
- MessageQueue如何存储消息,以什么结构存储?
- 你提到了Looper,请问子线程如何获取Looper?
- 你提到了Looper,说一说Looper的消息队列模型?
- 主线程Looper为什么不会阻塞?为什么不会ANR?
- 子线程跟主线程如何通过Handler通信?
- 子线程创建Handler这么麻烦,有什么替代方法吗?
- 主线程Looper什么时候启动的?
- 对Handler做过哪些优化?
Handler源码分析#
Handler#构造函数原理#
Handler有7个构造函数
Handler()
Handler(Handler.Callback callback)
Handler(Looper looper)
Handler(Looper looper, Handler.Callback callback)
Handler(boolean async)
Handler(Callback callback, boolean async)
Handler(Looper looper, Callback callback, boolean async)
先从其中一个构造函数看起:
提到了MessageQueue、Looper,Message,CallBack暂且记下。
我们根据经验及面试题,关注Handler几个关键API
- obtainMessage
- post
- sendMessage
- postDelayed
- sendMessageDelayed
接着我们关注这些API的底层实现,一个一个分析吧!
Handler#obtainMessage 原理#
Handler#obtainMessage 调用了Message#obtain()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T7alw6p4-1597413794308)(C:\Users\lenovo\AppData\Local\Temp\1597329263919.png)]
可以看到obtain函数从Message链表中获取message,这是一种内存复用,节省了频繁创建内存,如果Message链表为空,则创建一个Message。如果你对Message是个链表有疑问,那么请继续看下面的内容吧!
Message源码分析#
Message有如下公有属性,供程序员调用:
1 | public int what;//消息标示, |
Message有如下私有属性,用途如下:
1 | /*package*/ static final int FLAG_IN_USE = 1 << 0;//正在使用中 |
Message的源码,我们可以得出如下结论,Message是一种链表结构,每个Message持有以下信息:
- 用于传递的数据,如what、arg1、arg2、obj
- 用于执行当前Message的Handler
- 用于执行当前Message的回调接口CallBack、子线程Runnable
- 当前Message的属性,如延时时间、执行标识、Bundle数据,下一个Message引用。这种结构构成了链表。
Handler#post的原理#
post函数入口接收一个子线程Runnable对象
1 | public final boolean post(Runnable r) |
getPostMessage()做了如下工作:
1 | private static Message getPostMessage(Runnable r) { |
sendMessageDelayed做了如下工作:
1 | public final boolean sendMessageDelayed(Message msg, long delayMillis) |
sendMessageAtTime做了如下工作:
1 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { |
最终走下了MessageQueue#enqueueMessage
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
这一步总结如下
- getPostMessage将Message与Handler绑定
- 通过SystemClock.uptimeMillis() + delayMillis计算延时时间,delayMillis默认为0
- 将Message与计算得出的时间值,传递给MessageQueue#enqueueMessage,交由MessageQueue处理Message。
Handler#sendMessage原理#
入口接收Message对象
1 | public final boolean sendMessage(Message msg) |
后续执行过程与post相同,最终都将Message交由MessageQueue#enqueueMessage处理。
Handler#postDelayed原理#
与post相比,postDelayed函数入口除了接收Runnable子线程对象,还接收一个时间戳,用于延时时间的计算。其他过程与post相同。
1 | public final boolean postDelayed(Runnable r, long delayMillis) |
Handler#sendMessageDelayed原理#
与sendMessage类似,多了一个时间戳,用于计算延时时间。其他过程与sendMessage、相同。
1 | public final boolean sendMessageDelayed(Message msg, long delayMillis) |
看完源码,我们得出几个结论:
- 无论是Handler#post或者是Handler#sendMessage,Messag都会交由MessageQueue#enqueueMessage执行
- MessageQueue#enqueueMessage接收两个参数Message和long型的时间戳
- 时间戳计算方式是SystemClock.uptimeMillis() + delayMillis
- post与sendMessage的区别是入参参数不一样,post接收Runnable子线程,将子线程绑定到Message上;sendMessage持有的是主线程
那么我们心中很自然会产生疑问,MessageQueue#enqueueMessage是如何执行的?
MessageQueue源码分析#
源码分析的思路是构造函数和enqueueMessage
MessageQueue#构造函数原理#
首先来看看构造函数
1 | MessageQueue(boolean quitAllowed) { |
构造函数之上定义了很多native方法
1 | private native static long nativeInit(); |
native之上定义了几类数据结构,Message、ArrayList、SparseArray、数组
1 | Message mMessages; // 头结点 |
接着我们来看看enqueueMessage是如何处理Message的吧
MessageQueue#enqueueMessage原理#
- 图中 1处会判断如果 Message 中的 target 没有被设置,则直接抛出异常;
- 图中2和 3 处会按照 Message 的时间 when 来有序得插入 MessageQueue 中,可以看出 MessageQueue 实际上是一个链表维护的有序队列,只不过是按照 Message 的执行时间来排序。
看到这里,思路似乎终止了,我们跟随Handler、MessQueue的脚步,只看到了Message被插入到MessageQueue的私有队列中。那我们产生的Message什么时候会背消费呢?
视角再次回到一开始的地方——Handler的构造函数原理,在那一节我们提到了Handler构造函数初始化Looper.myLooper(),mLooper.mQueue,接下来我们看看Looper吧!
Looper源码分析#
查看源码可知,Looper是final类型的,禁止被外部继承修改。
Looper#子线程用例#
首先在Looper类的注释上,我们看到了如下信息,提示我们在子线程中用个Looper.prepare()+Looper.looper()的方式使用Handler
为什么需要用这种方式开启Looper呢?
答案是在任何线程要开启Loop,都要用Looper.prepare()+Looper.looper()的方式。以APP主进程为例,APP进程启动入口的main方法,也是通过这种方式开启loop的。与子线程细微不同的是,主线程开启looper用的是prepareMainLooper。
ActivityThread #main方法
带着以下疑问,我们去追看源码:Looper构造函数做了什么?prepare做了什么?loop做了什么?
Looper#构造函数原理#
1 | private Looper(boolean quitAllowed) { |
Looper构造函数做了两件事情,初始化消息队列MessageQueue对象,记录当前线程信息。
Looper#myLooper()原理#
1 | /** |
可以看到myLooper是从threadLocal中取出Looper对象。在Looper类中定义了如下变量sThreadLocal、mQueue、sMainLooper、mThread
1 | // sThreadLocal.get() will return null unless you've called prepare(). |
Looper#prepare原理#
1 | private static void prepare(boolean quitAllowed) { |
prepare就是 new 出一个 Looper。核心之处在于将 new 出的 Looper 设置到了线程本地变量 sThreadLocal 中。也就是说创建的 Looper 与当前线程发生了绑定。
Looper#prepareMainLooper原理
1 | public static void prepareMainLooper() { |
prepareMainLooper只有在APP进程启动的时候有用,并不推荐开发者调用这个函数。
Looper#loop原理#
图1 取出Looper对象
图2 校验当前线程是否持有Looper,是否启动而来Looper.prepare
图3 从Looper中取出对应的MessageQueue,主线程Looper就取出主线程的MessageQueue,子线程就取出子线程MessageQueue
图4 从MessageQueue中取出Message
图5 Message#target属性,即handler,调用Message绑定好的handler#dispatchMessage,处理消息。
也就是说,Message最终交由与Message绑定的Handler处理。Looper只是负责无限循环+从MessageQueue中读取。
Handler#dispatchMessage#
1 | /** |
可以看到有3处可以处理Message
图 1触发了Message#Runnable的run方法,要知道callback就是个Runnable子线程
1 | private static void handleCallback(Message message) { |
图2 触发了 Handler#Callback接口,Callback是Handler构造函数初始化的时候传递进来的。参考Handler#构造函数原理
1 | public interface Callback { |
图3 触发了Handler的handleMessage方法,这是个空实现,一般由开发者复写实现。
1 | /** |
在Looper这一节,我们暂停脚步总结一下:
主线程和子线程都可以使用Handler,Handler使用方式都是要Looper.prepare+Lopper.loop,
子线程Handler用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
new LooperThread().start();
回答这些面试题吧
面试题解析#
哪些场景使用到了Handler?用Handler做什么业务?#
最简单的消息发送#
主线程使用Handler, 主线程里或子线程里发送消息,或延迟发送消息的方式更新UI如,启动应用时Splash页面的延迟2,3秒后,跳转到主页面
1 | private static final int WHAT_UPDATE_ICON = 1; |
使用消息的时候,尽量使用 obtainMessage 的方式来获取Message,避免多次创建Message对象,消耗内存,效率低下。
记住:消息不一定是更新UI的消息,可以再handlerMessage中做很多事情!
结合HandlerThread处理耗时任务#
结合HandlerThread,串行的处理单个耗时任务,如单任务下载
1 | class DownloadOneByOne extends HandlerThread { |
倒计时View的简易实现#
通过Handler我们还可以快速简易,并且不占用太多性能的实现一个简易的倒计时View。
1 | public class CountDownView extends AppCompatTextView { |
结合IntentService的使用#
使用IntentService处理耗时的任务相对比较简单,我们来个有难度的,结合AlarmManager的调度,息屏唤醒IntentService定时处理任务的案例来讲
1 | private static final String ACTION_WAKE_UP = "com.doze.cpu.wakeup"; |
定时5分钟发送一个Action为com.doze.cpu.wakeup的广播,我们的广播需要继承 WakefulBroadcastReceiver, 在onReceive里,调用startWakefulService方法,会创建一个1分钟的WakeLock,唤醒cpu处理我们的任务,我们的任务IntentService处理最好不过了,处理完就销毁,不会有多余的占用
1 | public class WakeCPUReceiver extends WakefulBroadcastReceiver { |
startWakefulService的源码
1 | public static ComponentName startWakefulService(Context context, Intent intent) { |
在IntentService里,我们在onHandleIntent处理我们的任务后,再调用WakefulBroadcastReceiver的静态方法completeWakefulIntent,释放WakeLock,减少电量的消耗
1 | public class WorkService extends IntentService { |
completeWakefulIntent源码
1 | public static boolean completeWakefulIntent(Intent intent) { |
IdleHandler 用于UI性能优化#
先计算任务放在Activity绘制结束完成之后,节省了90Ms计算时间。参考面试题你了解过IdleHandler 吗?
HandlerThread用于单线程消息通知器#
在用户操作某些界面元素的时候,如收藏、点赞、转发,有一个小的问题,就是如果有一个操作生成10个快速连续的增删改查操作,那么我们的UI就会收到10次回调,而这种场景下我们其实只需要最后一次回调就够了,中间操作其实不用刷新UI的。如何合并这些频繁操作,只在最后一次操作结束时候响应UI更新呢。
答:HandlerThread+反射MessageQueue+idelHandler
用Handler遇到什么问题?怎么解决这些问题的?#
问题:Handler延时不准确,经常到了时间不响应业务#
解决:SystemClock.uptimeMillis()**表示系统开机到当前的时间总数**,单位是毫秒,但是,当系统进入深度睡眠(CPU休眠、屏幕休眠、设备等待外部输入)时间就会停止,但是不会受到时钟缩放、空闲或者其他节能机制的影响。
使用其他延时方式
- 用concurrent包的TimeUnit类延时sleep()方法延时
- Timer+TimeTask
- AlarmManager
- ScheduledExecutorService
问题:子线程创建Handler失败#
解决:参考Looper#子线程用例部分
问题:非静态类导致的内存泄漏#
解决:static+WeakReference
1 | static class MyHandler extends Handler{ |
说一说Handler原理?#
原理的定义是:某个类的提供哪些功能,这些功能是如何实现的?
我认为回答这个问题包含三步:Handler是什么,关键API是什么,关键对象是什么?
第一步:回答Handler是什么,有哪些场景。#
答:Handler是Android消息通信组件,用于线程间通信,收发消息,更新UI等参考面试题目1。
第二步:Handler的关键API是什么,用途是什么,如何实现的。#
答:关键API有:
构造函数:用于绑定MessageQueue、Looper、Message、Runnable、CallBack等
obtainMessage:用于复用Message
post、sendMessage:不同的执行Message方式,前者是接收Runnable参数,后者是当前线程
sendMessageAtTime:消息延时的实现入口,调用MessageQueue#enqueueMessage
第三步:Handler的关键对象是什么,提供哪些功能,这些功能如何实现的。#
答:关键对象有:
Message
Message是一种链表结构的子节点,作为载体可以存储的信息有:公开信息arg1、arg2、handler、Message,私有信息when、Runnable。Message有2种Flag,使用中、同步中,三种消息类型,如普通消息、异步消息、消息屏障。
MessageQueue
MessageQueue提供一种链表数据结构,包括头结点信息,插入节点的方式是按照 Message 的时间 when 顺序,时间小的先插入 。
Looper
开启无限循环,不断从 MessageQueue 中取出 Message,然后处理 Message 中指定的任务。典型的Looper是主线程Looper,在 ActivityThread 的 main 方法中,除了调用 Looper.prepareMainLooper 初始化 Looper 对象之外,还调用了 Looper.loop 方法开启无限循环,Looper 的主要功能就是在这个循环中完成的。
Looper提供了一些native方法用于唤醒阻塞状态如nativePollOnce
Looper不断loop的结果,就是调用msg.target.handleMessage,即执行开发者定义好的Handler#handleMessage方法体中的业务。
能自己实现一个Handler吗?#
根据Handler的类图,我们可以抽象出Handler消息组件的基本架构。
简版Handler#
概要设计:
首先我们仿照Android的Handler定义了:阻塞队列、处理消息的回调、分发和发送消息的方法
其次然后在创建Handler时,我们获取了当前线程的Looper和MessageQueue
最后,当我们发送消息的时候,将消息添加进之前得到的MessageQueue
1 | public class MyHandler { |
简版Looper#
- 在Looper中,我们用一个ThreadLocal存储当前Looper的相关数据
- 定义了一个消息队列,用来管理消息
- 在prepare()时,用ThreadLocal存储Looper的数据;在myLooper时,读取ThreadLocal存储的Looper数据
- 在loop()时,用一个死循环来不断的接受和分发消息
1 | public class MyLooper { |
简版MessageQueue#
1 | public class MyMessageQueue { |
简版Message#
1 | public class MyMessage { |
写一个网络请求的测试用例#
1 | public class TestClient { |
说一说Handler延时原理?#
首先Handler无论是post还是sendMessage方式处理Message过程中,都会产生一个时间戳,计算方式是SystemClock.uptimeMillis() + delayMillis,这个时间戳会赋值给Message.when,影响Message在MessageQueue链表中的位置。时间戳值越大,越晚执行。
Handler延时存在时间不准的问题,问题产生原因以及解决办法以及在面试题2问题:Handler延时不准确,经常到了时间不响应业务提到。
Handler延时有哪些缺陷?造成这些缺陷的原因?#
参考面试题2问题:Handler延时不准确,经常到了时间不响应业务
Handler的post与sendMessage有哪些区别?
post需要指定Runnable参数,将传入的Runnable绑定至Handler默认的Message,很多值都为默认值,换言之post方法只是为了执行Runnable子线程的任务。
sendMessage需要传入开发者自定义的Message参数,将Message中的信息载体传递下去,sendMessage方法是为了传递消息。
两者最终都会将Message传递下去,区别是Message中的数据信息赋值数量的不同。
子线程能使用Handler吗?#
能,可以使用handler对象以及对应的方法。区别是Handler的创建位置,如果Handler在主线程创建,那么只能在主线程中处理消息。如果在子线程创建Handler,那么才能在子线程处理消息。
子线程能创建Handler吗?#
能,前提是需要Looper.prepare+Looper.loop
Looper.prepare是将当前线程添加到sThreadLocal中,Looper.loop是开启无限循环,不断执行Message
子线程创建Handler这么麻烦,有什么替代方法吗?了解过HandlerThread吗?#
HandlerThread的run方法中替我们做了Looper.prepare+Looper.loop
1 | HandlerThread handlerThread = new HandlerThread("handler-thread"); |
了解过IdleHandler吗?#
IdleHandler 用途:
- IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机;
- 当 MessageQueue 当前没有立即需要处理的消息时,会执行 IdleHandler;
- Activity界面绘制结束的回调时机
IdleHandler 缺点:
- 但它执行的时机依赖消息队列的情况,那么如果 MessageQueue 一直有待执行的消息时,IdleHandler 就一直得不到执行,也就是它的执行时机是不可控的,不适合执行一些对时机要求比较高的任务。
IdleHandler场景
如果我们想在界面绘制出来后做点什么,那么在onResume里面是不合适的,它先于measure等流程了, **有人可能会说在onResume里面post一个runnable可以吗?还是不行,因为那样就会变成这个样子
所以你的行为一样会在绘制之前执行,这个时候我们的主角IdleHandler就发挥作用了,我们前面说了,它是在looper里面message暂时执行完毕了就会回调,顾名思义嘛,Idle就是队列为空的意思,那么我们的onResume和measure, layout, draw都是一个个message的话,这个IdleHandler就提供了一个它们都执行完毕的回调了,大概就是这样
也就是说IdleHandler可以再界面绘制的消息回调之后执行。
优化前:
这个是我们地图的公交详情页面, 进入之后产品要求左边的页卡需要展示,可以看到左边的页卡是一个非常复杂的布局,那么进入之后的效果可以明显看到头部的展示信息是先显示空白再100毫秒左右之后才展示出来的,原因就是这个页卡的内容比较复杂,用数据向它填充的时候花了较长时间,代码如下:
可以看到这个detailView就是这个侧滑的页卡了,填充里面的数据花了90ms,如果这个时间是用在了界面view绘制之前的话,就会出现以上的效果了,view先是白的,再出现,这样就体验不好了。
优化后:如果我们把它放到IdleHandler里面呢?
结果非常明显:顶部的页卡先展示出来了,这样体验是不是会更好一些呢。虽然只有短短90ms,不过我们做app也应该关注这种细节优化的,是吧~ 这个做法也提供了一种思路,android本身提供的activity框架和fragment框架并没有提供绘制完成的回调,如果我们自己实现一个框架,就可以使用这个IdleHandler来实现一个onRenderFinished这种回调了。
代码如下:
特别参考